/*
 * Copyright 2002-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.mail.javamail;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Date;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.activation.FileTypeMap;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimePart;
import javax.mail.internet.MimeUtility;

import org.springframework.core.io.InputStreamSource;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
 * Helper class for populating a {@link javax.mail.internet.MimeMessage}.
 *
 * <p>Mirrors the simple setters of {@link org.springframework.mail.SimpleMailMessage},
 * directly applying the values to the underlying MimeMessage. Allows for defining
 * a character encoding for the entire message, automatically applied by all methods
 * of this helper class.
 *
 * <p>Offers support for HTML text content, inline elements such as images, and typical
 * mail attachments. Also supports personal names that accompany mail addresses. Note that
 * advanced settings can still be applied directly to the underlying MimeMessage object!
 *
 * <p>Typically used in {@link MimeMessagePreparator} implementations or
 * {@link JavaMailSender} client code: simply instantiating it as a MimeMessage wrapper,
 * invoking setters on the wrapper, using the underlying MimeMessage for mail sending.
 * Also used internally by {@link JavaMailSenderImpl}.
 *
 * <p>Sample code for an HTML mail with an inline image and a PDF attachment:
 *
 * <pre class="code">
 * mailSender.send(new MimeMessagePreparator() {
 *   public void prepare(MimeMessage mimeMessage) throws MessagingException {
 *     MimeMessageHelper message = new MimeMessageHelper(mimeMessage, true, "UTF-8");
 *     message.setFrom("me@mail.com");
 *     message.setTo("you@mail.com");
 *     message.setSubject("my subject");
 *     message.setText("my text &lt;img src='cid:myLogo'&gt;", true);
 *     message.addInline("myLogo", new ClassPathResource("img/mylogo.gif"));
 *     message.addAttachment("myDocument.pdf", new ClassPathResource("doc/myDocument.pdf"));
 *   }
 * });</pre>
 * <p>
 * Consider using {@link MimeMailMessage} (which implements the common
 * {@link org.springframework.mail.MailMessage} interface, just like
 * {@link org.springframework.mail.SimpleMailMessage}) on top of this helper,
 * in order to let message population code interact with a simple message
 * or a MIME message through a common interface.
 *
 * <p><b>Warning regarding multipart mails:</b> Simple MIME messages that
 * just contain HTML text but no inline elements or attachments will work on
 * more or less any email client that is capable of HTML rendering. However,
 * inline elements and attachments are still a major compatibility issue
 * between email clients: It's virtually impossible to get inline elements
 * and attachments working across Microsoft Outlook, Lotus Notes and Mac Mail.
 * Consider choosing a specific multipart mode for your needs: The javadoc
 * on the MULTIPART_MODE constants contains more detailed information.
 *
 * @author Juergen Hoeller
 * @see #setText(String, boolean)
 * @see #setText(String, String)
 * @see #addInline(String, org.springframework.core.io.Resource)
 * @see #addAttachment(String, org.springframework.core.io.InputStreamSource)
 * @see #MULTIPART_MODE_MIXED_RELATED
 * @see #MULTIPART_MODE_RELATED
 * @see #getMimeMessage()
 * @see JavaMailSender
 * @since 19.01.2004
 */
public class MimeMessageHelper {

    /**
     * Constant indicating a non-multipart message.
     */
    public static final int MULTIPART_MODE_NO = 0;

    /**
     * Constant indicating a multipart message with a single root multipart
     * element of type "mixed". Texts, inline elements and attachements
     * will all get added to that root element.
     * <p>This was Spring 1.0's default behavior. It is known to work properly
     * on Outlook. However, other mail clients tend to misinterpret inline
     * elements as attachments and/or show attachments inline as well.
     */
    public static final int MULTIPART_MODE_MIXED = 1;

    /**
     * Constant indicating a multipart message with a single root multipart
     * element of type "related". Texts, inline elements and attachements
     * will all get added to that root element.
     * <p>This was the default behavior from Spring 1.1 up to 1.2 final.
     * This is the "Microsoft multipart mode", as natively sent by Outlook.
     * It is known to work properly on Outlook, Outlook Express, Yahoo Mail, and
     * to a large degree also on Mac Mail (with an additional attachment listed
     * for an inline element, despite the inline element also shown inline).
     * Does not work properly on Lotus Notes (attachments won't be shown there).
     */
    public static final int MULTIPART_MODE_RELATED = 2;

    /**
     * Constant indicating a multipart message with a root multipart element
     * "mixed" plus a nested multipart element of type "related". Texts and
     * inline elements will get added to the nested "related" element,
     * while attachments will get added to the "mixed" root element.
     * <p>This is the default since Spring 1.2.1. This is arguably the most correct
     * MIME structure, according to the MIME spec: It is known to work properly
     * on Outlook, Outlook Express, Yahoo Mail, and Lotus Notes. Does not work
     * properly on Mac Mail. If you target Mac Mail or experience issues with
     * specific mails on Outlook, consider using MULTIPART_MODE_RELATED instead.
     */
    public static final int MULTIPART_MODE_MIXED_RELATED = 3;


    private static final String MULTIPART_SUBTYPE_MIXED = "mixed";

    private static final String MULTIPART_SUBTYPE_RELATED = "related";

    private static final String MULTIPART_SUBTYPE_ALTERNATIVE = "alternative";

    private static final String CONTENT_TYPE_ALTERNATIVE = "text/alternative";

    private static final String CONTENT_TYPE_HTML = "text/html";

    private static final String CONTENT_TYPE_CHARSET_SUFFIX = ";charset=";

    private static final String HEADER_PRIORITY = "X-Priority";

    private static final String HEADER_CONTENT_ID = "Content-ID";


    private final MimeMessage mimeMessage;
    @Nullable
    private final String encoding;
    @Nullable
    private MimeMultipart rootMimeMultipart;
    @Nullable
    private MimeMultipart mimeMultipart;
    private FileTypeMap fileTypeMap;

    private boolean validateAddresses = false;


    /**
     * Create a new MimeMessageHelper for the given MimeMessage,
     * assuming a simple text message (no multipart content,
     * i.e. no alternative texts and no inline elements or attachments).
     * <p>The character encoding for the message will be taken from
     * the passed-in MimeMessage object, if carried there. Else,
     * JavaMail's default encoding will be used.
     *
     * @param mimeMessage MimeMessage to work on
     * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, boolean)
     * @see #getDefaultEncoding(javax.mail.internet.MimeMessage)
     * @see JavaMailSenderImpl#setDefaultEncoding
     */
    public MimeMessageHelper(MimeMessage mimeMessage) {
        this(mimeMessage, null);
    }

    /**
     * Create a new MimeMessageHelper for the given MimeMessage,
     * assuming a simple text message (no multipart content,
     * i.e. no alternative texts and no inline elements or attachments).
     *
     * @param mimeMessage MimeMessage to work on
     * @param encoding the character encoding to use for the message
     * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, boolean)
     */
    public MimeMessageHelper(MimeMessage mimeMessage, @Nullable String encoding) {
        this.mimeMessage = mimeMessage;
        this.encoding = (encoding != null ? encoding : getDefaultEncoding(mimeMessage));
        this.fileTypeMap = getDefaultFileTypeMap(mimeMessage);
    }

    /**
     * Create a new MimeMessageHelper for the given MimeMessage,
     * in multipart mode (supporting alternative texts, inline
     * elements and attachments) if requested.
     * <p>Consider using the MimeMessageHelper constructor that
     * takes a multipartMode argument to choose a specific multipart
     * mode other than MULTIPART_MODE_MIXED_RELATED.
     * <p>The character encoding for the message will be taken from
     * the passed-in MimeMessage object, if carried there. Else,
     * JavaMail's default encoding will be used.
     *
     * @param mimeMessage MimeMessage to work on
     * @param multipart whether to create a multipart message that
     * supports alternative texts, inline elements and attachments
     * (corresponds to MULTIPART_MODE_MIXED_RELATED)
     * @throws MessagingException if multipart creation failed
     * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, int)
     * @see #getDefaultEncoding(javax.mail.internet.MimeMessage)
     * @see JavaMailSenderImpl#setDefaultEncoding
     */
    public MimeMessageHelper(MimeMessage mimeMessage, boolean multipart) throws MessagingException {
        this(mimeMessage, multipart, null);
    }

    /**
     * Create a new MimeMessageHelper for the given MimeMessage,
     * in multipart mode (supporting alternative texts, inline
     * elements and attachments) if requested.
     * <p>Consider using the MimeMessageHelper constructor that
     * takes a multipartMode argument to choose a specific multipart
     * mode other than MULTIPART_MODE_MIXED_RELATED.
     *
     * @param mimeMessage MimeMessage to work on
     * @param multipart whether to create a multipart message that
     * supports alternative texts, inline elements and attachments
     * (corresponds to MULTIPART_MODE_MIXED_RELATED)
     * @param encoding the character encoding to use for the message
     * @throws MessagingException if multipart creation failed
     * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, int, String)
     */
    public MimeMessageHelper(MimeMessage mimeMessage, boolean multipart, @Nullable String encoding)
            throws MessagingException {

        this(mimeMessage, (multipart ? MULTIPART_MODE_MIXED_RELATED : MULTIPART_MODE_NO), encoding);
    }

    /**
     * Create a new MimeMessageHelper for the given MimeMessage,
     * in multipart mode (supporting alternative texts, inline
     * elements and attachments) if requested.
     * <p>The character encoding for the message will be taken from
     * the passed-in MimeMessage object, if carried there. Else,
     * JavaMail's default encoding will be used.
     *
     * @param mimeMessage MimeMessage to work on
     * @param multipartMode which kind of multipart message to create
     * (MIXED, RELATED, MIXED_RELATED, or NO)
     * @throws MessagingException if multipart creation failed
     * @see #MULTIPART_MODE_NO
     * @see #MULTIPART_MODE_MIXED
     * @see #MULTIPART_MODE_RELATED
     * @see #MULTIPART_MODE_MIXED_RELATED
     * @see #getDefaultEncoding(javax.mail.internet.MimeMessage)
     * @see JavaMailSenderImpl#setDefaultEncoding
     */
    public MimeMessageHelper(MimeMessage mimeMessage, int multipartMode) throws MessagingException {
        this(mimeMessage, multipartMode, null);
    }

    /**
     * Create a new MimeMessageHelper for the given MimeMessage,
     * in multipart mode (supporting alternative texts, inline
     * elements and attachments) if requested.
     *
     * @param mimeMessage MimeMessage to work on
     * @param multipartMode which kind of multipart message to create
     * (MIXED, RELATED, MIXED_RELATED, or NO)
     * @param encoding the character encoding to use for the message
     * @throws MessagingException if multipart creation failed
     * @see #MULTIPART_MODE_NO
     * @see #MULTIPART_MODE_MIXED
     * @see #MULTIPART_MODE_RELATED
     * @see #MULTIPART_MODE_MIXED_RELATED
     */
    public MimeMessageHelper(MimeMessage mimeMessage, int multipartMode, @Nullable String encoding)
            throws MessagingException {

        this.mimeMessage = mimeMessage;
        createMimeMultiparts(mimeMessage, multipartMode);
        this.encoding = (encoding != null ? encoding : getDefaultEncoding(mimeMessage));
        this.fileTypeMap = getDefaultFileTypeMap(mimeMessage);
    }


    /**
     * Return the underlying MimeMessage object.
     */
    public final MimeMessage getMimeMessage() {
        return this.mimeMessage;
    }


    /**
     * Determine the MimeMultipart objects to use, which will be used
     * to store attachments on the one hand and text(s) and inline elements
     * on the other hand.
     * <p>Texts and inline elements can either be stored in the root element
     * itself (MULTIPART_MODE_MIXED, MULTIPART_MODE_RELATED) or in a nested element
     * rather than the root element directly (MULTIPART_MODE_MIXED_RELATED).
     * <p>By default, the root MimeMultipart element will be of type "mixed"
     * (MULTIPART_MODE_MIXED) or "related" (MULTIPART_MODE_RELATED).
     * The main multipart element will either be added as nested element of
     * type "related" (MULTIPART_MODE_MIXED_RELATED) or be identical to the root
     * element itself (MULTIPART_MODE_MIXED, MULTIPART_MODE_RELATED).
     *
     * @param mimeMessage the MimeMessage object to add the root MimeMultipart
     * object to
     * @param multipartMode the multipart mode, as passed into the constructor
     * (MIXED, RELATED, MIXED_RELATED, or NO)
     * @throws MessagingException if multipart creation failed
     * @see #setMimeMultiparts
     * @see #MULTIPART_MODE_NO
     * @see #MULTIPART_MODE_MIXED
     * @see #MULTIPART_MODE_RELATED
     * @see #MULTIPART_MODE_MIXED_RELATED
     */
    protected void createMimeMultiparts(MimeMessage mimeMessage, int multipartMode) throws MessagingException {
        switch (multipartMode) {
            case MULTIPART_MODE_NO:
                setMimeMultiparts(null, null);
                break;
            case MULTIPART_MODE_MIXED:
                MimeMultipart mixedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_MIXED);
                mimeMessage.setContent(mixedMultipart);
                setMimeMultiparts(mixedMultipart, mixedMultipart);
                break;
            case MULTIPART_MODE_RELATED:
                MimeMultipart relatedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_RELATED);
                mimeMessage.setContent(relatedMultipart);
                setMimeMultiparts(relatedMultipart, relatedMultipart);
                break;
            case MULTIPART_MODE_MIXED_RELATED:
                MimeMultipart rootMixedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_MIXED);
                mimeMessage.setContent(rootMixedMultipart);
                MimeMultipart nestedRelatedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_RELATED);
                MimeBodyPart relatedBodyPart = new MimeBodyPart();
                relatedBodyPart.setContent(nestedRelatedMultipart);
                rootMixedMultipart.addBodyPart(relatedBodyPart);
                setMimeMultiparts(rootMixedMultipart, nestedRelatedMultipart);
                break;
            default:
                throw new IllegalArgumentException("Only multipart modes MIXED_RELATED, RELATED and NO supported");
        }
    }

    /**
     * Set the given MimeMultipart objects for use by this MimeMessageHelper.
     *
     * @param root the root MimeMultipart object, which attachments will be added to;
     * or {@code null} to indicate no multipart at all
     * @param main the main MimeMultipart object, which text(s) and inline elements
     * will be added to (can be the same as the root multipart object, or an element
     * nested underneath the root multipart element)
     */
    protected final void setMimeMultiparts(@Nullable MimeMultipart root, @Nullable MimeMultipart main) {
        this.rootMimeMultipart = root;
        this.mimeMultipart = main;
    }

    /**
     * Return whether this helper is in multipart mode,
     * i.e. whether it holds a multipart message.
     *
     * @see #MimeMessageHelper(MimeMessage, boolean)
     */
    public final boolean isMultipart() {
        return (this.rootMimeMultipart != null);
    }

    /**
     * Return the root MIME "multipart/mixed" object, if any.
     * Can be used to manually add attachments.
     * <p>This will be the direct content of the MimeMessage,
     * in case of a multipart mail.
     *
     * @throws IllegalStateException if this helper is not in multipart mode
     * @see #isMultipart
     * @see #getMimeMessage
     * @see javax.mail.internet.MimeMultipart#addBodyPart
     */
    public final MimeMultipart getRootMimeMultipart() throws IllegalStateException {
        if (this.rootMimeMultipart == null) {
            throw new IllegalStateException("Not in multipart mode - " +
                    "create an appropriate MimeMessageHelper via a constructor that takes a 'multipart' flag " +
                    "if you need to set alternative texts or add inline elements or attachments.");
        }
        return this.rootMimeMultipart;
    }

    /**
     * Return the underlying MIME "multipart/related" object, if any.
     * Can be used to manually add body parts, inline elements, etc.
     * <p>This will be nested within the root MimeMultipart,
     * in case of a multipart mail.
     *
     * @throws IllegalStateException if this helper is not in multipart mode
     * @see #isMultipart
     * @see #getRootMimeMultipart
     * @see javax.mail.internet.MimeMultipart#addBodyPart
     */
    public final MimeMultipart getMimeMultipart() throws IllegalStateException {
        if (this.mimeMultipart == null) {
            throw new IllegalStateException("Not in multipart mode - " +
                    "create an appropriate MimeMessageHelper via a constructor that takes a 'multipart' flag " +
                    "if you need to set alternative texts or add inline elements or attachments.");
        }
        return this.mimeMultipart;
    }


    /**
     * Determine the default encoding for the given MimeMessage.
     *
     * @param mimeMessage the passed-in MimeMessage
     * @return the default encoding associated with the MimeMessage,
     * or {@code null} if none found
     */
    @Nullable
    protected String getDefaultEncoding(MimeMessage mimeMessage) {
        if (mimeMessage instanceof SmartMimeMessage) {
            return ((SmartMimeMessage) mimeMessage).getDefaultEncoding();
        }
        return null;
    }

    /**
     * Return the specific character encoding used for this message, if any.
     */
    @Nullable
    public String getEncoding() {
        return this.encoding;
    }

    /**
     * Determine the default Java Activation FileTypeMap for the given MimeMessage.
     *
     * @param mimeMessage the passed-in MimeMessage
     * @return the default FileTypeMap associated with the MimeMessage,
     * or a default ConfigurableMimeFileTypeMap if none found for the message
     * @see ConfigurableMimeFileTypeMap
     */
    protected FileTypeMap getDefaultFileTypeMap(MimeMessage mimeMessage) {
        if (mimeMessage instanceof SmartMimeMessage) {
            FileTypeMap fileTypeMap = ((SmartMimeMessage) mimeMessage).getDefaultFileTypeMap();
            if (fileTypeMap != null) {
                return fileTypeMap;
            }
        }
        ConfigurableMimeFileTypeMap fileTypeMap = new ConfigurableMimeFileTypeMap();
        fileTypeMap.afterPropertiesSet();
        return fileTypeMap;
    }

    /**
     * Return the {@code FileTypeMap} used by this MimeMessageHelper.
     */
    public FileTypeMap getFileTypeMap() {
        return this.fileTypeMap;
    }

    /**
     * Set the Java Activation Framework {@code FileTypeMap} to use
     * for determining the content type of inline content and attachments
     * that get added to the message.
     * <p>Default is the {@code FileTypeMap} that the underlying
     * MimeMessage carries, if any, or the Activation Framework's default
     * {@code FileTypeMap} instance else.
     *
     * @see #addInline
     * @see #addAttachment
     * @see #getDefaultFileTypeMap(javax.mail.internet.MimeMessage)
     * @see JavaMailSenderImpl#setDefaultFileTypeMap
     * @see javax.activation.FileTypeMap#getDefaultFileTypeMap
     * @see ConfigurableMimeFileTypeMap
     */
    public void setFileTypeMap(@Nullable FileTypeMap fileTypeMap) {
        this.fileTypeMap = (fileTypeMap != null ? fileTypeMap : getDefaultFileTypeMap(getMimeMessage()));
    }

    /**
     * Return whether this helper will validate all addresses passed to it.
     */
    public boolean isValidateAddresses() {
        return this.validateAddresses;
    }

    /**
     * Set whether to validate all addresses which get passed to this helper.
     * Default is "false".
     * <p>Note that this is by default just available for JavaMail >= 1.3.
     * You can override the default {@code validateAddress method} for
     * validation on older JavaMail versions (or for custom validation).
     *
     * @see #validateAddress
     */
    public void setValidateAddresses(boolean validateAddresses) {
        this.validateAddresses = validateAddresses;
    }

    /**
     * Validate the given mail address.
     * Called by all of MimeMessageHelper's address setters and adders.
     * <p>Default implementation invokes {@code InternetAddress.validate()},
     * provided that address validation is activated for the helper instance.
     * <p>Note that this method will just work on JavaMail >= 1.3. You can override
     * it for validation on older JavaMail versions or for custom validation.
     *
     * @param address the address to validate
     * @throws AddressException if validation failed
     * @see #isValidateAddresses()
     * @see javax.mail.internet.InternetAddress#validate()
     */
    protected void validateAddress(InternetAddress address) throws AddressException {
        if (isValidateAddresses()) {
            address.validate();
        }
    }

    /**
     * Validate all given mail addresses.
     * Default implementation simply delegates to validateAddress for each address.
     *
     * @param addresses the addresses to validate
     * @throws AddressException if validation failed
     * @see #validateAddress(InternetAddress)
     */
    protected void validateAddresses(InternetAddress[] addresses) throws AddressException {
        for (InternetAddress address : addresses) {
            validateAddress(address);
        }
    }


    public void setFrom(InternetAddress from) throws MessagingException {
        Assert.notNull(from, "From address must not be null");
        validateAddress(from);
        this.mimeMessage.setFrom(from);
    }

    public void setFrom(String from) throws MessagingException {
        Assert.notNull(from, "From address must not be null");
        setFrom(parseAddress(from));
    }

    public void setFrom(String from, String personal) throws MessagingException, UnsupportedEncodingException {
        Assert.notNull(from, "From address must not be null");
        setFrom(getEncoding() != null ?
                new InternetAddress(from, personal, getEncoding()) : new InternetAddress(from, personal));
    }

    public void setReplyTo(InternetAddress replyTo) throws MessagingException {
        Assert.notNull(replyTo, "Reply-to address must not be null");
        validateAddress(replyTo);
        this.mimeMessage.setReplyTo(new InternetAddress[] {replyTo});
    }

    public void setReplyTo(String replyTo) throws MessagingException {
        Assert.notNull(replyTo, "Reply-to address must not be null");
        setReplyTo(parseAddress(replyTo));
    }

    public void setReplyTo(String replyTo, String personal) throws MessagingException, UnsupportedEncodingException {
        Assert.notNull(replyTo, "Reply-to address must not be null");
        InternetAddress replyToAddress = (getEncoding() != null) ?
                                         new InternetAddress(replyTo, personal, getEncoding())
                                                                 : new InternetAddress(replyTo, personal);
        setReplyTo(replyToAddress);
    }


    public void setTo(InternetAddress to) throws MessagingException {
        Assert.notNull(to, "To address must not be null");
        validateAddress(to);
        this.mimeMessage.setRecipient(Message.RecipientType.TO, to);
    }

    public void setTo(InternetAddress[] to) throws MessagingException {
        Assert.notNull(to, "To address array must not be null");
        validateAddresses(to);
        this.mimeMessage.setRecipients(Message.RecipientType.TO, to);
    }

    public void setTo(String to) throws MessagingException {
        Assert.notNull(to, "To address must not be null");
        setTo(parseAddress(to));
    }

    public void setTo(String[] to) throws MessagingException {
        Assert.notNull(to, "To address array must not be null");
        InternetAddress[] addresses = new InternetAddress[to.length];
        for (int i = 0; i < to.length; i++) {
            addresses[i] = parseAddress(to[i]);
        }
        setTo(addresses);
    }

    public void addTo(InternetAddress to) throws MessagingException {
        Assert.notNull(to, "To address must not be null");
        validateAddress(to);
        this.mimeMessage.addRecipient(Message.RecipientType.TO, to);
    }

    public void addTo(String to) throws MessagingException {
        Assert.notNull(to, "To address must not be null");
        addTo(parseAddress(to));
    }

    public void addTo(String to, String personal) throws MessagingException, UnsupportedEncodingException {
        Assert.notNull(to, "To address must not be null");
        addTo(getEncoding() != null ?
              new InternetAddress(to, personal, getEncoding()) :
              new InternetAddress(to, personal));
    }


    public void setCc(InternetAddress cc) throws MessagingException {
        Assert.notNull(cc, "Cc address must not be null");
        validateAddress(cc);
        this.mimeMessage.setRecipient(Message.RecipientType.CC, cc);
    }

    public void setCc(InternetAddress[] cc) throws MessagingException {
        Assert.notNull(cc, "Cc address array must not be null");
        validateAddresses(cc);
        this.mimeMessage.setRecipients(Message.RecipientType.CC, cc);
    }

    public void setCc(String cc) throws MessagingException {
        Assert.notNull(cc, "Cc address must not be null");
        setCc(parseAddress(cc));
    }

    public void setCc(String[] cc) throws MessagingException {
        Assert.notNull(cc, "Cc address array must not be null");
        InternetAddress[] addresses = new InternetAddress[cc.length];
        for (int i = 0; i < cc.length; i++) {
            addresses[i] = parseAddress(cc[i]);
        }
        setCc(addresses);
    }

    public void addCc(InternetAddress cc) throws MessagingException {
        Assert.notNull(cc, "Cc address must not be null");
        validateAddress(cc);
        this.mimeMessage.addRecipient(Message.RecipientType.CC, cc);
    }

    public void addCc(String cc) throws MessagingException {
        Assert.notNull(cc, "Cc address must not be null");
        addCc(parseAddress(cc));
    }

    public void addCc(String cc, String personal) throws MessagingException, UnsupportedEncodingException {
        Assert.notNull(cc, "Cc address must not be null");
        addCc(getEncoding() != null ?
              new InternetAddress(cc, personal, getEncoding()) :
              new InternetAddress(cc, personal));
    }


    public void setBcc(InternetAddress bcc) throws MessagingException {
        Assert.notNull(bcc, "Bcc address must not be null");
        validateAddress(bcc);
        this.mimeMessage.setRecipient(Message.RecipientType.BCC, bcc);
    }

    public void setBcc(InternetAddress[] bcc) throws MessagingException {
        Assert.notNull(bcc, "Bcc address array must not be null");
        validateAddresses(bcc);
        this.mimeMessage.setRecipients(Message.RecipientType.BCC, bcc);
    }

    public void setBcc(String bcc) throws MessagingException {
        Assert.notNull(bcc, "Bcc address must not be null");
        setBcc(parseAddress(bcc));
    }

    public void setBcc(String[] bcc) throws MessagingException {
        Assert.notNull(bcc, "Bcc address array must not be null");
        InternetAddress[] addresses = new InternetAddress[bcc.length];
        for (int i = 0; i < bcc.length; i++) {
            addresses[i] = parseAddress(bcc[i]);
        }
        setBcc(addresses);
    }

    public void addBcc(InternetAddress bcc) throws MessagingException {
        Assert.notNull(bcc, "Bcc address must not be null");
        validateAddress(bcc);
        this.mimeMessage.addRecipient(Message.RecipientType.BCC, bcc);
    }

    public void addBcc(String bcc) throws MessagingException {
        Assert.notNull(bcc, "Bcc address must not be null");
        addBcc(parseAddress(bcc));
    }

    public void addBcc(String bcc, String personal) throws MessagingException, UnsupportedEncodingException {
        Assert.notNull(bcc, "Bcc address must not be null");
        addBcc(getEncoding() != null ?
               new InternetAddress(bcc, personal, getEncoding()) :
               new InternetAddress(bcc, personal));
    }

    private InternetAddress parseAddress(String address) throws MessagingException {
        InternetAddress[] parsed = InternetAddress.parse(address);
        if (parsed.length != 1) {
            throw new AddressException("Illegal address", address);
        }
        InternetAddress raw = parsed[0];
        try {
            return (getEncoding() != null ?
                    new InternetAddress(raw.getAddress(), raw.getPersonal(), getEncoding()) : raw);
        } catch (UnsupportedEncodingException ex) {
            throw new MessagingException("Failed to parse embedded personal name to correct encoding", ex);
        }
    }


    /**
     * Set the priority ("X-Priority" header) of the message.
     *
     * @param priority the priority value;
     * typically between 1 (highest) and 5 (lowest)
     * @throws MessagingException in case of errors
     */
    public void setPriority(int priority) throws MessagingException {
        this.mimeMessage.setHeader(HEADER_PRIORITY, Integer.toString(priority));
    }

    /**
     * Set the sent-date of the message.
     *
     * @param sentDate the date to set (never {@code null})
     * @throws MessagingException in case of errors
     */
    public void setSentDate(Date sentDate) throws MessagingException {
        Assert.notNull(sentDate, "Sent date must not be null");
        this.mimeMessage.setSentDate(sentDate);
    }

    /**
     * Set the subject of the message, using the correct encoding.
     *
     * @param subject the subject text
     * @throws MessagingException in case of errors
     */
    public void setSubject(String subject) throws MessagingException {
        Assert.notNull(subject, "Subject must not be null");
        if (getEncoding() != null) {
            this.mimeMessage.setSubject(subject, getEncoding());
        } else {
            this.mimeMessage.setSubject(subject);
        }
    }


    /**
     * Set the given text directly as content in non-multipart mode
     * or as default body part in multipart mode.
     * Always applies the default content type "text/plain".
     * <p><b>NOTE:</b> Invoke {@link #addInline} <i>after</i> {@code setText};
     * else, mail readers might not be able to resolve inline references correctly.
     *
     * @param text the text for the message
     * @throws MessagingException in case of errors
     */
    public void setText(String text) throws MessagingException {
        setText(text, false);
    }

    /**
     * Set the given text directly as content in non-multipart mode
     * or as default body part in multipart mode.
     * The "html" flag determines the content type to apply.
     * <p><b>NOTE:</b> Invoke {@link #addInline} <i>after</i> {@code setText};
     * else, mail readers might not be able to resolve inline references correctly.
     *
     * @param text the text for the message
     * @param html whether to apply content type "text/html" for an
     * HTML mail, using default content type ("text/plain") else
     * @throws MessagingException in case of errors
     */
    public void setText(String text, boolean html) throws MessagingException {
        Assert.notNull(text, "Text must not be null");
        MimePart partToUse;
        if (isMultipart()) {
            partToUse = getMainPart();
        } else {
            partToUse = this.mimeMessage;
        }
        if (html) {
            setHtmlTextToMimePart(partToUse, text);
        } else {
            setPlainTextToMimePart(partToUse, text);
        }
    }

    /**
     * Set the given plain text and HTML text as alternatives, offering
     * both options to the email client. Requires multipart mode.
     * <p><b>NOTE:</b> Invoke {@link #addInline} <i>after</i> {@code setText};
     * else, mail readers might not be able to resolve inline references correctly.
     *
     * @param plainText the plain text for the message
     * @param htmlText the HTML text for the message
     * @throws MessagingException in case of errors
     */
    public void setText(String plainText, String htmlText) throws MessagingException {
        Assert.notNull(plainText, "Plain text must not be null");
        Assert.notNull(htmlText, "HTML text must not be null");

        MimeMultipart messageBody = new MimeMultipart(MULTIPART_SUBTYPE_ALTERNATIVE);
        getMainPart().setContent(messageBody, CONTENT_TYPE_ALTERNATIVE);

        // Create the plain text part of the message.
        MimeBodyPart plainTextPart = new MimeBodyPart();
        setPlainTextToMimePart(plainTextPart, plainText);
        messageBody.addBodyPart(plainTextPart);

        // Create the HTML text part of the message.
        MimeBodyPart htmlTextPart = new MimeBodyPart();
        setHtmlTextToMimePart(htmlTextPart, htmlText);
        messageBody.addBodyPart(htmlTextPart);
    }

    private MimeBodyPart getMainPart() throws MessagingException {
        MimeMultipart mimeMultipart = getMimeMultipart();
        MimeBodyPart bodyPart = null;
        for (int i = 0; i < mimeMultipart.getCount(); i++) {
            BodyPart bp = mimeMultipart.getBodyPart(i);
            if (bp.getFileName() == null) {
                bodyPart = (MimeBodyPart) bp;
            }
        }
        if (bodyPart == null) {
            MimeBodyPart mimeBodyPart = new MimeBodyPart();
            mimeMultipart.addBodyPart(mimeBodyPart);
            bodyPart = mimeBodyPart;
        }
        return bodyPart;
    }

    private void setPlainTextToMimePart(MimePart mimePart, String text) throws MessagingException {
        if (getEncoding() != null) {
            mimePart.setText(text, getEncoding());
        } else {
            mimePart.setText(text);
        }
    }

    private void setHtmlTextToMimePart(MimePart mimePart, String text) throws MessagingException {
        if (getEncoding() != null) {
            mimePart.setContent(text, CONTENT_TYPE_HTML + CONTENT_TYPE_CHARSET_SUFFIX + getEncoding());
        } else {
            mimePart.setContent(text, CONTENT_TYPE_HTML);
        }
    }


    /**
     * Add an inline element to the MimeMessage, taking the content from a
     * {@code javax.activation.DataSource}.
     * <p>Note that the InputStream returned by the DataSource implementation
     * needs to be a <i>fresh one on each call</i>, as JavaMail will invoke
     * {@code getInputStream()} multiple times.
     * <p><b>NOTE:</b> Invoke {@code addInline} <i>after</i> {@link #setText};
     * else, mail readers might not be able to resolve inline references correctly.
     *
     * @param contentId the content ID to use. Will end up as "Content-ID" header
     * in the body part, surrounded by angle brackets: e.g. "myId" -> "&lt;myId&gt;".
     * Can be referenced in HTML source via src="cid:myId" expressions.
     * @param dataSource the {@code javax.activation.DataSource} to take
     * the content from, determining the InputStream and the content type
     * @throws MessagingException in case of errors
     * @see #addInline(String, java.io.File)
     * @see #addInline(String, org.springframework.core.io.Resource)
     */
    public void addInline(String contentId, DataSource dataSource) throws MessagingException {
        Assert.notNull(contentId, "Content ID must not be null");
        Assert.notNull(dataSource, "DataSource must not be null");
        MimeBodyPart mimeBodyPart = new MimeBodyPart();
        mimeBodyPart.setDisposition(MimeBodyPart.INLINE);
        // We're using setHeader here to remain compatible with JavaMail 1.2,
        // rather than JavaMail 1.3's setContentID.
        mimeBodyPart.setHeader(HEADER_CONTENT_ID, "<" + contentId + ">");
        mimeBodyPart.setDataHandler(new DataHandler(dataSource));
        getMimeMultipart().addBodyPart(mimeBodyPart);
    }

    /**
     * Add an inline element to the MimeMessage, taking the content from a
     * {@code java.io.File}.
     * <p>The content type will be determined by the name of the given
     * content file. Do not use this for temporary files with arbitrary
     * filenames (possibly ending in ".tmp" or the like)!
     * <p><b>NOTE:</b> Invoke {@code addInline} <i>after</i> {@link #setText};
     * else, mail readers might not be able to resolve inline references correctly.
     *
     * @param contentId the content ID to use. Will end up as "Content-ID" header
     * in the body part, surrounded by angle brackets: e.g. "myId" -> "&lt;myId&gt;".
     * Can be referenced in HTML source via src="cid:myId" expressions.
     * @param file the File resource to take the content from
     * @throws MessagingException in case of errors
     * @see #setText
     * @see #addInline(String, org.springframework.core.io.Resource)
     * @see #addInline(String, javax.activation.DataSource)
     */
    public void addInline(String contentId, File file) throws MessagingException {
        Assert.notNull(file, "File must not be null");
        FileDataSource dataSource = new FileDataSource(file);
        dataSource.setFileTypeMap(getFileTypeMap());
        addInline(contentId, dataSource);
    }

    /**
     * Add an inline element to the MimeMessage, taking the content from a
     * {@code org.springframework.core.io.Resource}.
     * <p>The content type will be determined by the name of the given
     * content file. Do not use this for temporary files with arbitrary
     * filenames (possibly ending in ".tmp" or the like)!
     * <p>Note that the InputStream returned by the Resource implementation
     * needs to be a <i>fresh one on each call</i>, as JavaMail will invoke
     * {@code getInputStream()} multiple times.
     * <p><b>NOTE:</b> Invoke {@code addInline} <i>after</i> {@link #setText};
     * else, mail readers might not be able to resolve inline references correctly.
     *
     * @param contentId the content ID to use. Will end up as "Content-ID" header
     * in the body part, surrounded by angle brackets: e.g. "myId" -> "&lt;myId&gt;".
     * Can be referenced in HTML source via src="cid:myId" expressions.
     * @param resource the resource to take the content from
     * @throws MessagingException in case of errors
     * @see #setText
     * @see #addInline(String, java.io.File)
     * @see #addInline(String, javax.activation.DataSource)
     */
    public void addInline(String contentId, Resource resource) throws MessagingException {
        Assert.notNull(resource, "Resource must not be null");
        String contentType = getFileTypeMap().getContentType(resource.getFilename());
        addInline(contentId, resource, contentType);
    }

    /**
     * Add an inline element to the MimeMessage, taking the content from an
     * {@code org.springframework.core.InputStreamResource}, and
     * specifying the content type explicitly.
     * <p>You can determine the content type for any given filename via a Java
     * Activation Framework's FileTypeMap, for example the one held by this helper.
     * <p>Note that the InputStream returned by the InputStreamSource implementation
     * needs to be a <i>fresh one on each call</i>, as JavaMail will invoke
     * {@code getInputStream()} multiple times.
     * <p><b>NOTE:</b> Invoke {@code addInline} <i>after</i> {@code setText};
     * else, mail readers might not be able to resolve inline references correctly.
     *
     * @param contentId the content ID to use. Will end up as "Content-ID" header
     * in the body part, surrounded by angle brackets: e.g. "myId" -> "&lt;myId&gt;".
     * Can be referenced in HTML source via src="cid:myId" expressions.
     * @param inputStreamSource the resource to take the content from
     * @param contentType the content type to use for the element
     * @throws MessagingException in case of errors
     * @see #setText
     * @see #getFileTypeMap
     * @see #addInline(String, org.springframework.core.io.Resource)
     * @see #addInline(String, javax.activation.DataSource)
     */
    public void addInline(String contentId, InputStreamSource inputStreamSource, String contentType)
            throws MessagingException {

        Assert.notNull(inputStreamSource, "InputStreamSource must not be null");
        if (inputStreamSource instanceof Resource && ((Resource) inputStreamSource).isOpen()) {
            throw new IllegalArgumentException(
                    "Passed-in Resource contains an open stream: invalid argument. " +
                            "JavaMail requires an InputStreamSource that creates a fresh stream for every call.");
        }
        DataSource dataSource = createDataSource(inputStreamSource, contentType, "inline");
        addInline(contentId, dataSource);
    }

    /**
     * Add an attachment to the MimeMessage, taking the content from a
     * {@code javax.activation.DataSource}.
     * <p>Note that the InputStream returned by the DataSource implementation
     * needs to be a <i>fresh one on each call</i>, as JavaMail will invoke
     * {@code getInputStream()} multiple times.
     *
     * @param attachmentFilename the name of the attachment as it will
     * appear in the mail (the content type will be determined by this)
     * @param dataSource the {@code javax.activation.DataSource} to take
     * the content from, determining the InputStream and the content type
     * @throws MessagingException in case of errors
     * @see #addAttachment(String, org.springframework.core.io.InputStreamSource)
     * @see #addAttachment(String, java.io.File)
     */
    public void addAttachment(String attachmentFilename, DataSource dataSource) throws MessagingException {
        Assert.notNull(attachmentFilename, "Attachment filename must not be null");
        Assert.notNull(dataSource, "DataSource must not be null");
        try {
            MimeBodyPart mimeBodyPart = new MimeBodyPart();
            mimeBodyPart.setDisposition(MimeBodyPart.ATTACHMENT);
            mimeBodyPart.setFileName(MimeUtility.encodeText(attachmentFilename));
            mimeBodyPart.setDataHandler(new DataHandler(dataSource));
            getRootMimeMultipart().addBodyPart(mimeBodyPart);
        } catch (UnsupportedEncodingException ex) {
            throw new MessagingException("Failed to encode attachment filename", ex);
        }
    }

    /**
     * Add an attachment to the MimeMessage, taking the content from a
     * {@code java.io.File}.
     * <p>The content type will be determined by the name of the given
     * content file. Do not use this for temporary files with arbitrary
     * filenames (possibly ending in ".tmp" or the like)!
     *
     * @param attachmentFilename the name of the attachment as it will
     * appear in the mail
     * @param file the File resource to take the content from
     * @throws MessagingException in case of errors
     * @see #addAttachment(String, org.springframework.core.io.InputStreamSource)
     * @see #addAttachment(String, javax.activation.DataSource)
     */
    public void addAttachment(String attachmentFilename, File file) throws MessagingException {
        Assert.notNull(file, "File must not be null");
        FileDataSource dataSource = new FileDataSource(file);
        dataSource.setFileTypeMap(getFileTypeMap());
        addAttachment(attachmentFilename, dataSource);
    }

    /**
     * Add an attachment to the MimeMessage, taking the content from an
     * {@code org.springframework.core.io.InputStreamResource}.
     * <p>The content type will be determined by the given filename for
     * the attachment. Thus, any content source will be fine, including
     * temporary files with arbitrary filenames.
     * <p>Note that the InputStream returned by the InputStreamSource
     * implementation needs to be a <i>fresh one on each call</i>, as
     * JavaMail will invoke {@code getInputStream()} multiple times.
     *
     * @param attachmentFilename the name of the attachment as it will
     * appear in the mail
     * @param inputStreamSource the resource to take the content from
     * (all of Spring's Resource implementations can be passed in here)
     * @throws MessagingException in case of errors
     * @see #addAttachment(String, java.io.File)
     * @see #addAttachment(String, javax.activation.DataSource)
     * @see org.springframework.core.io.Resource
     */
    public void addAttachment(String attachmentFilename, InputStreamSource inputStreamSource)
            throws MessagingException {

        String contentType = getFileTypeMap().getContentType(attachmentFilename);
        addAttachment(attachmentFilename, inputStreamSource, contentType);
    }

    /**
     * Add an attachment to the MimeMessage, taking the content from an
     * {@code org.springframework.core.io.InputStreamResource}.
     * <p>Note that the InputStream returned by the InputStreamSource
     * implementation needs to be a <i>fresh one on each call</i>, as
     * JavaMail will invoke {@code getInputStream()} multiple times.
     *
     * @param attachmentFilename the name of the attachment as it will
     * appear in the mail
     * @param inputStreamSource the resource to take the content from
     * (all of Spring's Resource implementations can be passed in here)
     * @param contentType the content type to use for the element
     * @throws MessagingException in case of errors
     * @see #addAttachment(String, java.io.File)
     * @see #addAttachment(String, javax.activation.DataSource)
     * @see org.springframework.core.io.Resource
     */
    public void addAttachment(
            String attachmentFilename, InputStreamSource inputStreamSource, String contentType)
            throws MessagingException {

        Assert.notNull(inputStreamSource, "InputStreamSource must not be null");
        if (inputStreamSource instanceof Resource && ((Resource) inputStreamSource).isOpen()) {
            throw new IllegalArgumentException(
                    "Passed-in Resource contains an open stream: invalid argument. " +
                            "JavaMail requires an InputStreamSource that creates a fresh stream for every call.");
        }
        DataSource dataSource = createDataSource(inputStreamSource, contentType, attachmentFilename);
        addAttachment(attachmentFilename, dataSource);
    }

    /**
     * Create an Activation Framework DataSource for the given InputStreamSource.
     *
     * @param inputStreamSource the InputStreamSource (typically a Spring Resource)
     * @param contentType the content type
     * @param name the name of the DataSource
     * @return the Activation Framework DataSource
     */
    protected DataSource createDataSource(
            final InputStreamSource inputStreamSource, final String contentType, final String name) {

        return new DataSource() {
            @Override
            public InputStream getInputStream() throws IOException {
                return inputStreamSource.getInputStream();
            }

            @Override
            public OutputStream getOutputStream() {
                throw new UnsupportedOperationException("Read-only javax.activation.DataSource");
            }

            @Override
            public String getContentType() {
                return contentType;
            }

            @Override
            public String getName() {
                return name;
            }
        };
    }

}
